﻿using S7Cat.Enums;
using S7Cat.Protocol;
using S7Cat.Tools;
using System.Buffers.Binary;
using System.Net.Sockets;

namespace S7Cat
{
    public abstract partial class BasePlc
    {
        public const int DEFAULT_PORT = 102;

        protected TcpClient? _tcpClient;
        protected NetworkStream? _stream;

        private readonly TsapPair TsapPair;

        public string IP { get; init; }
        public int Port { get; init; }

        public int MaxPDU { get; private set; }

        public int ReadTimeout
        {
            get;
            set
            {
                field = value;
                _tcpClient?.ReceiveTimeout = value;
            }
        }

        public int WriteTimeout
        {
            get;
            set
            {
                field = value;
                _tcpClient?.SendTimeout = value;
            }
        }

        public bool IsConnected => _tcpClient != null && _tcpClient.Connected;

        internal BasePlc(CpuType cpu, string ip, int port, short rack, short slot)
        {
            IP = ip;
            Port = port;

            TsapPair = TsapPair.GetDefaultTsapPair(cpu, rack, slot);
        }


        public async Task OpenAsync(CancellationToken cancellationToken = default)
        {
            _tcpClient = new TcpClient()
            {
                ReceiveTimeout = ReadTimeout,
                SendTimeout = WriteTimeout,
            };

            try
            {
                await _tcpClient.ConnectAsync(IP, Port, cancellationToken).ConfigureAwait(false);
                _stream = _tcpClient.GetStream();

                await EstablishConnection(cancellationToken).ConfigureAwait(false);
            }
            catch (Exception)
            {
                _stream = null;
                _tcpClient?.Dispose();
                _tcpClient = null;
                throw;
            }
        }

        public void Close()
        {
            if (_tcpClient != null)
            {
                if (_tcpClient.Connected)
                {
                    _tcpClient.Close();
                }
                _tcpClient.Dispose();
                _tcpClient = null; // Can not reuse _tcpClient once connection gets closed.
            }
        }

        private async Task EstablishConnection(CancellationToken cancellationToken)
        {
            await RequestConnection(cancellationToken).ConfigureAwait(false);
            await SetupConnection(cancellationToken).ConfigureAwait(false);
        }

        private async Task RequestConnection(CancellationToken cancellationToken)
        {
            var data = ConnectionRequest.GetCOTPConnectionRequest(TsapPair);
            var response = await RequestTPDUAsync(data, cancellationToken).ConfigureAwait(false);

            if (response.PduType != PduType.ConnectionConfirmed)
            {
                throw new Exception("Connection request was denied");
            }
        }

        private async Task SetupConnection(CancellationToken cancellationToken)
        {
            var setupData = ConnectionRequest.GetS7ConnectionSetup();
            using var s7data = await RequestTsduAsync(setupData, cancellationToken)
                .ConfigureAwait(false);

            if (s7data.Length < 20)
            {
                throw new Exception("Not enough data received in response to Communication Setup");
            }

            var span = s7data.Span;
            if (span[1] != 0x03)
            {
                throw new InvalidDataException("Error reading Communication Setup response");
            }

            MaxPDU = BinaryPrimitives.ReadInt16BigEndian(span.Slice(18, 2));
        }

        private async Task<TPDU> RequestTPDUAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken)
        {
            await _stream!.WriteAsync(data, cancellationToken).ConfigureAwait(false);
            return await TPDU.ReadAsync(_stream, cancellationToken).ConfigureAwait(false);
        }

        internal virtual async Task<MemoryOwner<byte>> RequestTsduAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken)
        {
            await _stream!.WriteAsync(data, cancellationToken).ConfigureAwait(false);
            return await TSDU.ReadAsync(_stream, cancellationToken).ConfigureAwait(false);
        }
    }
}
